Làm chủ vòng lặp bất đồng bộ trong JavaScript bằng vòng lặp 'for await...of' và các trình trợ giúp async iterator tùy chỉnh. Nâng cao khả năng xử lý luồng và dữ liệu với các ví dụ thực tế.
Trình trợ giúp JavaScript Async Iterator: For Each - Lặp xử lý luồng
Lập trình bất đồng bộ là nền tảng của phát triển JavaScript hiện đại, cho phép các ứng dụng xử lý các hoạt động tốn thời gian mà không chặn luồng chính. Async iterators, được giới thiệu trong ECMAScript 2018, cung cấp một cơ chế mạnh mẽ để xử lý các luồng dữ liệu một cách bất đồng bộ. Bài viết blog này đi sâu vào khái niệm về async iterators và minh họa cách triển khai một hàm trợ giúp 'for each' bất đồng bộ để tinh giản quá trình xử lý luồng.
Tìm hiểu về Async Iterators
Một async iterator là một đối tượng tuân thủ giao diện AsyncIterator. Nó định nghĩa một phương thức next() trả về một promise, promise này sẽ giải quyết thành một đối tượng có hai thuộc tính:
value: Giá trị tiếp theo trong chuỗi.done: Một giá trị boolean cho biết liệu iterator đã hoàn thành hay chưa.
Async iterators thường được sử dụng để tiêu thụ dữ liệu từ các nguồn bất đồng bộ như luồng mạng, hệ thống tệp tin, hoặc cơ sở dữ liệu. Vòng lặp for await...of cung cấp một cú pháp tiện lợi để lặp qua các async iterables.
Ví dụ: Đọc tệp một cách bất đồng bộ
Hãy xem xét một kịch bản bạn cần đọc một tệp lớn theo từng dòng mà không chặn luồng chính. Bạn có thể đạt được điều này bằng cách sử dụng một async iterator:
const fs = require('fs');
const readline = require('readline');
async function* readFileLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
async function processFile(filePath) {
for await (const line of readFileLines(filePath)) {
console.log(`Line: ${line}`);
}
}
// Example usage
processFile('path/to/your/file.txt');
Trong ví dụ này, readFileLines là một hàm async generator, nó yield mỗi dòng của tệp khi được đọc. Hàm processFile sau đó lặp qua các dòng bằng cách sử dụng for await...of, xử lý mỗi dòng một cách bất đồng bộ.
Triển khai Trình trợ giúp 'For Each' Bất đồng bộ
Mặc dù vòng lặp for await...of rất hữu ích, nó có thể trở nên dài dòng khi bạn cần thực hiện các hoạt động phức tạp trên mỗi phần tử trong luồng. Một hàm trợ giúp 'for each' bất đồng bộ có thể đơn giản hóa quá trình này bằng cách đóng gói logic lặp.
Triển khai cơ bản
Đây là một triển khai cơ bản của hàm 'for each' bất đồng bộ:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
await callback(item);
}
}
Hàm này nhận một async iterable và một hàm callback làm đối số. Nó lặp qua iterable bằng cách sử dụng for await...of và gọi hàm callback cho mỗi mục. Hàm callback cũng nên là bất đồng bộ nếu bạn muốn đợi nó hoàn thành trước khi chuyển sang mục tiếp theo.
Ví dụ: Xử lý dữ liệu từ API
Giả sử bạn đang lấy dữ liệu từ một API trả về một luồng các mục. Bạn có thể sử dụng trình trợ giúp 'for each' bất đồng bộ để xử lý mỗi mục khi nó đến:
async function* fetchDataStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
// Assuming the API returns JSON chunks
const chunk = decoder.decode(value);
const items = JSON.parse(`[${chunk.replace(/\}\{/g, '},{')}]`); //Split chunks into valid json array
for(const item of items){
yield item;
}
}
} finally {
reader.releaseLock();
}
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem);
console.log('Finished processing data.');
}
// Example usage
main();
Trong ví dụ này, fetchDataStream lấy dữ liệu từ API và yield mỗi mục khi nó được nhận. Hàm processItem mô phỏng một hoạt động bất đồng bộ trên mỗi mục. Trình trợ giúp asyncForEach sau đó đơn giản hóa logic lặp và xử lý.
Cải tiến và những điều cần cân nhắc
Xử lý lỗi
Việc xử lý các lỗi có thể xảy ra trong quá trình lặp bất đồng bộ là rất quan trọng. Bạn có thể bao bọc hàm callback trong một khối try...catch để bắt và xử lý các ngoại lệ:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
try {
await callback(item);
} catch (error) {
console.error(`Error processing item: ${item}`, error);
// You can choose to re-throw the error or continue processing
}
}
}
Kiểm soát đồng thời
Theo mặc định, trình trợ giúp 'for each' bất đồng bộ xử lý các mục một cách tuần tự. Nếu bạn cần xử lý các mục đồng thời, bạn có thể sử dụng một nhóm Promise (Promise pool) để giới hạn số lượng hoạt động đồng thời:
async function asyncForEachConcurrent(iterable, callback, concurrency) {
const executing = [];
for await (const item of iterable) {
const p = callback(item).then(() => executing.splice(executing.indexOf(p), 1));
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEachConcurrent(fetchDataStream(apiUrl), processItem, 5); // Concurrency of 5
console.log('Finished processing data.');
}
Trong ví dụ này, asyncForEachConcurrent giới hạn số lượng thực thi callback đồng thời theo mức độ đồng thời đã chỉ định. Điều này có thể cải thiện hiệu suất khi xử lý các luồng dữ liệu lớn.
Hủy bỏ
Trong một số trường hợp, bạn có thể cần hủy bỏ quá trình lặp sớm. Bạn có thể đạt được điều này bằng cách sử dụng một AbortController:
async function asyncForEach(iterable, callback, signal) {
for await (const item of iterable) {
if (signal && signal.aborted) {
console.log('Iteration aborted.');
return;
}
await callback(item);
}
}
async function main() {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort(); // Abort after 2 seconds
}, 2000);
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem, signal);
console.log('Finished processing data.');
}
Trong ví dụ này, hàm asyncForEach kiểm tra thuộc tính signal.aborted trước mỗi lần lặp. Nếu tín hiệu bị hủy, vòng lặp sẽ dừng lại.
Ứng dụng trong thế giới thực
Async iterators và trình trợ giúp 'for each' bất đồng bộ có thể được áp dụng cho nhiều kịch bản trong thế giới thực:
- Data processing pipelines: Xử lý các tập dữ liệu lớn từ cơ sở dữ liệu hoặc hệ thống tệp tin.
- Real-time data streams: Xử lý dữ liệu từ web sockets, hàng đợi tin nhắn, hoặc mạng cảm biến.
- API consumption: Lấy và xử lý dữ liệu từ các API trả về luồng các mục.
- Image and video processing: Xử lý các tệp đa phương tiện lớn theo từng khối.
- Log analysis: Phân tích các tệp nhật ký lớn theo từng dòng.
Ví dụ - Dữ liệu chứng khoán quốc tế: Hãy xem xét một ứng dụng lấy báo giá chứng khoán thời gian thực từ các sàn giao dịch quốc tế khác nhau. Một async iterator có thể được sử dụng để truyền dữ liệu, và một 'for each' bất đồng bộ có thể xử lý mỗi báo giá, cập nhật giao diện người dùng với giá mới nhất. Điều này có thể được sử dụng để hiển thị tỷ giá cổ phiếu hiện tại của các công ty như:
- Tencent (Trung Quốc): Lấy dữ liệu chứng khoán của một công ty công nghệ quốc tế lớn
- Tata Consultancy Services (Ấn Độ): Hiển thị các cập nhật chứng khoán từ một công ty dịch vụ CNTT hàng đầu
- Samsung Electronics (Hàn Quốc): Trình bày tỷ giá cổ phiếu từ một nhà sản xuất điện tử toàn cầu
- Toyota Motor Corporation (Nhật Bản): Theo dõi giá cổ phiếu của một nhà sản xuất ô tô quốc tế
Kết luận
Async iterators và trình trợ giúp 'for each' bất đồng bộ cung cấp một cách mạnh mẽ và tinh tế để xử lý các luồng dữ liệu một cách bất đồng bộ trong JavaScript. Bằng cách đóng gói logic lặp, bạn có thể đơn giản hóa mã nguồn, cải thiện khả năng đọc và nâng cao hiệu suất của ứng dụng. Bằng cách xử lý lỗi, kiểm soát đồng thời và cho phép hủy bỏ, bạn có thể tạo ra các quy trình xử lý dữ liệu bất đồng bộ mạnh mẽ và có khả năng mở rộng.